{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению\n",
    "<center>Автор материала: Котенков Игорь (@stalkermustang)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center> \n",
    "### Оптимизация параметров с помощью hyperopt\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Столкнувшись с оптимизацией гиперпараметров моделей в hw8 и почитав слак, я понял, что столь немаловажный аспект практически не затронут в курсе и вызывает немало вопросов. Чтож, давайте исправим ситуацию и окунемся в чудесный многомерный мир пространств параметров. <br/>\n",
    "Для воспроизведения кода понадобится установить библиотеку hyperopt. Остальное за нас подготовили создатели пакета Anaconda.\n",
    "Итак,"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install hyperopt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "#необходимые импорты\n",
    "from hyperopt import STATUS_OK, Trials, fmin, hp, rand, tpe\n",
    "from sklearn.cross_validation import train_test_split\n",
    "from sklearn.datasets import load_iris\n",
    "\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Вводные"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Думаю, все знают, для чего мы перебираем комбинации (или \"тюним\" модель) параметров - значения по умолчанию, а так же эмпирические догадки практически всегда не дают желаемой/оптимальной точности, показывают далеко не лучшие результаты в соревнованиях и проде. Пример из жизни, который многим проходящим курс ODS будет близок - 8-ая домашка. Vowpal Wabbit без конкретных параметров показывал ужасную оценку(на секунду я даже подумал, что не смогу получить баллы) - больше 1.15 MAE(целевая метка - для тех, кто не в курсе, логарифмирована). Однако даже без добавления фич простой оптимизацией за 30 минут нашлось решение, побившее установленный бенчмарк(около 0.90 MAE)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "С целью уменьшения количества итераций и времени перебора конфигураций были придуманы адаптивные байесовские методы. Они, в отличие от простого GridSearch(который работает в дискретном пространстве и имеет шанс пропустить оптимум, лежащий между ячейками сетки, как в примере ниже. Даже RandomSearch будет лучше), берут следующее приближение и вычисляют в нем значение функции опираясь на предыдущие проверки. Концепция заключается в нахождении некоторого баланса между поиском более удачных комбинаций около уже известных \"хороших\" точек и проверкой значений в неизведанных областях, где с ненулевой вероятностью могут находиться решения получше. Мне встречалось обозначение \"learning vs earning\" или \"дилеммой explore-exploit\" - можете блеснуть знаниями в кругу друзей или на митапе: обилие иностранных слов и терминов может даже помочь в трудоустройстве без собеседования(шутка - никогда не стоит говорить о том, в чем сам не разобрался. Перед использованием прогуглить). <br/>\n",
    "Данная методика позволяет сходиться к более удачным решениям и за более короткий срок(ведь на больших датасетах прогон одной модели может обходитсья достаточно дорого в разрезе вычислительных мощностей). <br/>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src='http://5047-presscdn.pagely.netdna-cdn.com/wp-content/uploads/2015/07/scikitlearn8.jpeg'>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данной библиотеке реализован интересный аглоритм для оптимизации TPE, или Tree-Structured Parzen Estimators. Подробно с ним можно ознакомиться [по ссылке](https://papers.nips.cc/paper/4443-algorithms-for-hyper-parameter-optimization.pdf). Если коротко, то TPE берет некоторое пространство поиска с разными плотнотями вероятностей и накладывает на них нормальное(Гауссовское) распределение со смещением, равным координатам новой точки. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## К столу, джентльмены"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Основным оружием в арсенале Hyperopt'a является функция fmin(первая в импортах). Она принимает в себя функцию(с несколькими аргументами, имеющими значения по умолчанию) и возвращает оптимальные параметры, с которыми значение функции минимально(если вам надо максимизировать, то очевидно, что нужно сменить знак вашей функции на противоположный). Подробнее об аргументах:\n",
    "* Функция, которую надо минимизировать\n",
    "* Пространство поиска параметров\n",
    "* Объект для хранения истории поиска\n",
    "* Алгоритм поиска\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тот самый алгоритм поиска TPE можно заменить при желании на RandomSearch. Достаточно указать \n",
    "```python\n",
    "algo=hyperopt.tpe.suggest \n",
    "```\n",
    "или\n",
    "```python\n",
    "algo=hyperopt.random.suggest\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Неоспоримый плюс TPE в том, что он может работать со странными и непонятными пространствами: пусть один гиперпараметр непрерывный, другой дискретный, иной и вовсе категориальный. Не обязательно даже, чтобы данная комбинация имела хоть какой то смысл. \n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Пространство поиска тоже задается весьма легко. Нужно всего лишь указать тип распределения и его границы. Приведу тут основные типы, с остальными можно ознакомиться в репозитории самой библиотеки.\n",
    "```python\n",
    "hp.choice() #равновероятный выбор из множества\n",
    "hp.randint() #случайное целое число; random seed, например, т.к. нет корреляции между разноудаленными значениями\n",
    "hp.uniform() #равномерное непрерывное распределение\n",
    "hp.normal() #нормальное непрерывное распределение\n",
    "hp.lognormal() #логнормальное непрерывное распределение\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тут есть интересный трюк: можно перебирать не только параметры модели, но и сами модели. Однако не вижу смысла на нем останавливаться, так как обычно данные готовятся под конкретную модель; однако ж все таки можно упороться, сделать для каждой свои преобразования, фичи, как описывает пайплайны один из туториалов, смешать в одно и оставить считаться на ночь(главное ошибку глупую не допустить, кхе, были прецеденты). Вот простой пример:\n",
    "```python\n",
    "space = hp.choice('classifier_type', [\n",
    "    {\n",
    "        'type': 'naive_bayes',\n",
    "    },\n",
    "    {\n",
    "        'type': 'svm',\n",
    "        'C': hp.lognormal('svm_C', 0, 1),\n",
    "        'kernel': hp.choice('svm_kernel', [\n",
    "            {'ktype': 'linear'},\n",
    "            {'ktype': 'RBF', 'width': hp.lognormal('svm_rbf_width', 0, 1)},\n",
    "            ]),\n",
    "    },\n",
    "    {\n",
    "        'type': 'dtree',\n",
    "        'criterion': hp.choice('dtree_criterion', ['gini', 'entropy']),\n",
    "        'max_depth': hp.choice('dtree_max_depth',\n",
    "            [None, hp.qlognormal('dtree_max_depth_int', 3, 1, 1)]),\n",
    "    },\n",
    "    ])\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И да, можно сделать ТОТ САМЫЙ *AutoML* :)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"Объект для хранения истории поиска\", он же Trials, позволяет нам сохранять и переносить, а так же прерывать и продолжать процесс перебора и поиска гиперпараматров. Очень удобно, кстати  - новый создаем командой\n",
    "```Python\n",
    "trials = Trials()\n",
    "```\n",
    ", а если захотим продолжить со старого места(скажем, накинуть еще 100 итераций или же если остановили kernel, так как подумали, что соверишили ошибку/любая другая ситуация), то просто не будем пересоздавать объект, а укажем старый объект аргументом вызываемой функции *fmin*\n",
    "```Python\n",
    "best = fmin(objective,\n",
    "    space=hp.uniform('x', -10, 10),\n",
    "    algo=tpe.suggest,\n",
    "    max_evals=100,\n",
    "    trials=trials)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь стоит отметить, что прерывать поиск раньше заданного количества итераций не стоит. Это объясняется тем, что при поиске и выборе новых точек для проверки одним из критериев как раз и выступает оставшийся \"баланс\" проверок(эволюций). Сначала пространство замеряется в разных точках(условно бьется), а затем уточняется. Поэтому *trials* могут стать вашими друзьями. Вместе с этим они хранят все результаты вычисления заданной функции и статусы выполнения(можно отлавливать ошибки). \n",
    "<br/>Поставили на ночь перебор, утром остановили, сделали pickle, ушли на работу, вернулись и продолжили перебор после загрузки trials. Вещь!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И, наконец, самое главное. Сама функция для оптимизации. Она должна принимать словарь значений гиперпараметров и возвращать значение целевой функции и, по желанию, статус. Простой пример\n",
    "```Python\n",
    "def objective(x):\n",
    "    return {'loss': x ** 2, 'status': STATUS_OK }\n",
    "```\n",
    ", где STATUS_OK - объект, импортированный нами в начале(см. импорты). "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на игрушечный пример(они нагляднее и понятнее всего, чего уж спорить) и сравним random и tpe."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#зададим пространство поиска\n",
    "space = [hp.uniform('x', -5, 5), hp.normal('y', -2, 2)] \n",
    "\n",
    "#укажем objective-функцию\n",
    "def f(args):\n",
    "    x, y = args\n",
    "    return (x+2)**2+y**2-1\n",
    "\n",
    "best = fmin(f, space, algo = tpe.suggest, max_evals=100)\n",
    "print ('TPE result: ', best)\n",
    "\n",
    "best = fmin(f, space, algo = rand.suggest, max_evals=100)\n",
    "print ('random result: ', best)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тут очевидно, что оптимум достигается в точке (-2.0; 0). Наиболее подходящую комбинацию аргументов (где дотигается минимальное значение функции) нашел именно TPE. То же применимо и к сложным многомерным структурам. <br/> Более того - предположим, что функция имеет локальные минимумы. При неудачном начальном приближении градиентный спуск не может выбраться из низины без использования специальных техник. Посмотрим, что сможет показать Hyperopt."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def f(args):\n",
    "    x = args\n",
    "    if x>=3:\n",
    "        return (x-8.3)**2\n",
    "    else:\n",
    "        return (x+2)**2+3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = np.linspace(-5, 10, 1000)\n",
    "y = [f(i) for i in x]\n",
    "\n",
    "plt.plot(x, y);\n",
    "\n",
    "space = hp.normal('x', -10, 10)\n",
    "\n",
    "best = fmin(f, space, algo = tpe.suggest, max_evals=50)\n",
    "print ('TPE result: ', best)\n",
    "\n",
    "best = fmin(f, space, algo = rand.suggest, max_evals=50)\n",
    "print ('random result: ', best)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Любое начальное приближение x < 3 давало бы другой оптимум в разрезе градиентного спуска. <br/>\n",
    "Удивляет и RandomSearch - он так же попался на уловку. Правда, для этого пришлось снизить количество итераций. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Заключение\n",
    "Hyperopt - мощное средство в руках умелого Data Scientist'a. Данная библиотека наиболее точно оценивает оптимальные параметры для заданных моделей, позволяет улучшать их результаты. С умным алгоритмом оптимизации TPE/байесовской оптимизацией ей не страшны локальные минимумы, ловушки разрывных распределений(причем, любых) и прочие узкие места остальных алгоритмов. "
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
